package utils

import (
	"context"
	"slices"
	"sort"

	"github.com/pkg/errors"
	"github.com/stackrox/rox/central/vulnmgmt/vulnerabilityrequest/common"
	v1 "github.com/stackrox/rox/generated/api/v1"
	"github.com/stackrox/rox/generated/storage"
	"github.com/stackrox/rox/pkg/grpc/authn"
	"github.com/stackrox/rox/pkg/protocompat"
	"github.com/stackrox/rox/pkg/search"
	"github.com/stackrox/rox/pkg/utils"
	"github.com/stackrox/rox/pkg/uuid"
)

// CreateRequestCommentProto creates *storage.RequestComment object.
func CreateRequestCommentProto(ctx context.Context, message string) *storage.RequestComment {
	if message == "" {
		return nil
	}
	return &storage.RequestComment{
		Id:        uuid.NewV4().String(),
		CreatedAt: protocompat.TimestampNow(),
		Message:   message,
		User:      authn.UserFromContext(ctx),
	}
}

// GetEnforcedRequestsV1Query returns a *v1.Query which will search for all enforced requests for specified CVEs.
func GetEnforcedRequestsV1Query(cve ...string) *v1.Query {
	return search.NewQueryBuilder().
		AddExactMatches(search.CVE, cve...).
		AddBools(search.ExpiredRequest, false).
		AddExactMatches(search.RequestStatus, storage.RequestStatus_APPROVED.String(), storage.RequestStatus_APPROVED_PENDING_UPDATE.String()).
		ProtoQuery()
}

// GetPendingRequestsV1Query returns a *v1.Query which will search for all pending requests for specified CVEs.
func GetPendingRequestsV1Query(cve ...string) *v1.Query {
	return search.NewQueryBuilder().
		AddExactMatches(search.CVE, cve...).
		AddBools(search.ExpiredRequest, false).
		AddExactMatches(search.RequestStatus, storage.RequestStatus_PENDING.String()).
		ProtoQuery()
}

// GetPendingUpdateRequestsV1Query returns a *v1.Query which will search for all pending update requests with specified CVEs
func GetPendingUpdateRequestsV1Query(cve ...string) *v1.Query {
	return search.ConjunctionQuery(
		search.NewQueryBuilder().
			AddBools(search.ExpiredRequest, false).
			AddExactMatches(search.RequestStatus, storage.RequestStatus_APPROVED_PENDING_UPDATE.String()).
			ProtoQuery(),
		search.DisjunctionQuery(
			search.NewQueryBuilder().AddExactMatches(search.DeferralUpdateCVEs, cve...).ProtoQuery(),
			search.NewQueryBuilder().AddExactMatches(search.FalsePositiveUpdateCVEs, cve...).ProtoQuery(),
		),
	)
}

// GetAffectedImagesQuery returns a *v1.Query object that can be used to fetch images affected by
// the vuln request as well as satisfying the incoming query.
func GetAffectedImagesQuery(request *storage.VulnerabilityRequest, query *v1.Query) (*v1.Query, error) {
	scopeQuery, err := GetImageQueryForVulnReq(request)
	if err != nil {
		return nil, err
	}
	if query == nil || query.GetQuery() == nil {
		return scopeQuery, nil
	}
	return search.ConjunctionQuery(query, scopeQuery), nil
}

// GetImageQueryForVulnReq returns a *v1.Query object that can be used to fetch images affected by the vuln request.
func GetImageQueryForVulnReq(request *storage.VulnerabilityRequest) (*v1.Query, error) {
	requestScope := request.GetScope()
	if requestScope.GetGlobalScope() != nil {
		return search.NewQueryBuilder().AddExactMatches(search.CVE, request.GetCves().GetCves()...).ProtoQuery(), nil
	}

	// Check if it is the v2 way of global scoping.
	if V2GlobalScope(requestScope) {
		return search.NewQueryBuilder().AddExactMatches(search.CVE, request.GetCves().GetCves()...).ProtoQuery(), nil
	}

	if imageScope := requestScope.GetImageScope(); imageScope != nil {
		queries := []*v1.Query{
			search.NewQueryBuilder().AddExactMatches(search.ImageRegistry, imageScope.GetRegistry()).ProtoQuery(),
			search.NewQueryBuilder().AddExactMatches(search.ImageRemote, imageScope.GetRemote()).ProtoQuery(),
		}
		if tagQ := GetTagQuery(imageScope.GetTag()); tagQ != nil {
			queries = append(queries, tagQ)
		}
		return search.ConjunctionQuery(queries...), nil
	}
	return nil, errors.New("scope must be provided for a vulnerability request")
}

// GetTagQuery returns a query can be sued to fetch all images satisfying the given tag.
// If the tag is empty, it pulls all the images not having a tag.
// If the tag is `.*`, it pulls all the images having empty or non-empty tag.
func GetTagQuery(tag string) *v1.Query {
	if tag == common.MatchAll {
		// If we want to match all tags, then exclude the query.
		return nil
	}
	if tag == "" {
		// Tag doesn't have to exist if deployed with a digest
		return search.DisjunctionQuery(
			search.NewQueryBuilder().AddExactMatches(search.ImageTag, "").ProtoQuery(),
			search.NewQueryBuilder().AddNullField(search.ImageTag).ProtoQuery())
	}
	return search.NewQueryBuilder().AddExactMatches(search.ImageTag, tag).ProtoQuery()
}

// GetActivePendingReqQuery returns a *v1.Query object that can be used to fetch active pending vuln requests.
func GetActivePendingReqQuery() *v1.Query {
	return search.ConjunctionQuery(
		search.NewQueryBuilder().AddBools(search.ExpiredRequest, false).ProtoQuery(),
		search.NewQueryBuilder().AddExactMatches(search.RequestStatus, storage.RequestStatus_PENDING.String(), storage.RequestStatus_APPROVED_PENDING_UPDATE.String()).ProtoQuery(),
	)
}

// GetActiveApprovedReqQuery returns a *v1.Query object that can be used to fetch active approved vuln requests.
func GetActiveApprovedReqQuery() *v1.Query {
	return search.ConjunctionQuery(
		search.NewQueryBuilder().AddBools(search.ExpiredRequest, false).ProtoQuery(),
		search.NewQueryBuilder().AddExactMatches(search.RequestStatus, storage.RequestStatus_APPROVED.String(), storage.RequestStatus_APPROVED_PENDING_UPDATE.String()).ProtoQuery(),
	)
}

// IsPending returns true if the original request or the update to original request is in pending state.
func IsPending(req *storage.VulnerabilityRequest) bool {
	return req.GetStatus() == storage.RequestStatus_PENDING || req.GetStatus() == storage.RequestStatus_APPROVED_PENDING_UPDATE
}

// FirstIndexMatchingScope returns the index of first vulnerability request in `matchWith` with scope covered by `toMatch`.
// If no match is found, -1 is returned.
// Note that this function does not check requested CVEs.
func FirstIndexMatchingScope(toMatch *storage.VulnerabilityRequest, matchWith []*storage.VulnerabilityRequest) int {
	for idx, r := range matchWith {
		// Check if the scopes match.
		if scopeCovered(r.GetScope(), toMatch.GetScope()) {
			return idx
		}
	}
	return -1
}

// RequestsWithCoveredScope returns all the requests whose scope is covered by the scope of `toMatch`.
// Note that this function does not check requested CVEs.
func RequestsWithCoveredScope(toMatch *storage.VulnerabilityRequest, matchWith []*storage.VulnerabilityRequest) []*storage.VulnerabilityRequest {
	if toMatch.GetScope() == nil {
		return nil
	}

	// If `toMatch` has global scope, then it covers all other scopes. Return early.
	if toMatch.GetScope().GetGlobalScope() != nil {
		return matchWith
	}

	// If `toMatch` has global scope according the v2 representation, then it covers all other scopes. Return early.
	if V2GlobalScope(toMatch.GetScope()) {
		return matchWith
	}

	var ret []*storage.VulnerabilityRequest
	for _, r := range matchWith {
		if scopeCovered(toMatch.GetScope(), r.GetScope()) {
			ret = append(ret, r)
		}
	}
	return ret
}

// V2GlobalScope returns true if the vulnerability exception scope is global scope according to the v2 representation.
func V2GlobalScope(scope *storage.VulnerabilityRequest_Scope) bool {
	if scope == nil {
		return false
	}
	imgScope := scope.GetImageScope()
	if imgScope == nil {
		return false
	}
	return imgScope.GetRegistry() == common.MatchAll &&
		imgScope.GetRemote() == common.MatchAll &&
		imgScope.GetTag() == common.MatchAll
}

// IsUpdateNoOp returns true if the exception update does not change the original exception.
func IsUpdateNoOp(req *storage.VulnerabilityRequest, update *common.UpdateRequest) bool {
	if req == nil || update == nil {
		return true
	}
	if update.FalsePositiveUpdate == nil && update.DeferralUpdate == nil {
		return true
	}

	existingCVEs := req.GetCves().GetCves()
	if deferralUpdate := update.DeferralUpdate; deferralUpdate != nil {
		newCVEs := deferralUpdate.GetCVEs()
		return equal(existingCVEs, newCVEs) &&
			req.GetDeferralReq().GetExpiry().EqualVT(deferralUpdate.GetExpiry())
	}

	if falsePositiveUpdate := update.FalsePositiveUpdate; falsePositiveUpdate != nil {
		newCVEs := falsePositiveUpdate.GetCVEs()
		return equal(existingCVEs, newCVEs)
	}

	// If we cannot determine that the update is a deferral update and false-positive, then we cannot handle it likely
	// because new proto message for updates was added. It must be treated as noop so that we do not reach the database.
	utils.Should(errors.Errorf("Unhandled vulnerability exception update: %v", update))
	return true
}

// ApproverV2 converts a slim user (v1 Approver type) to the v2 Approver type.
func ApproverV2(user *storage.SlimUser) *storage.Approver {
	if user == nil {
		return nil
	}
	return &storage.Approver{
		Id:   user.GetId(),
		Name: user.GetName(),
	}
}

func equal(a, b []string) bool {
	sort.SliceStable(a, func(i, j int) bool {
		return a[i] < a[j]
	})
	sort.SliceStable(b, func(i, j int) bool {
		return b[i] < b[j]
	})
	return slices.Equal(a, b)
}

// Returns true if the scope of `toMatch` covers the scope of `matchWith`.
func scopeCovered(toMatch *storage.VulnerabilityRequest_Scope, matchWith *storage.VulnerabilityRequest_Scope) bool {
	if toMatch == nil || matchWith == nil {
		return false
	}

	if toMatch.GetGlobalScope() != nil {
		return true
	}

	// If `toMatch` has global scope according the v2 representation, then it covers all other scope. Return early.
	if V2GlobalScope(toMatch) {
		return true
	}

	// If `toMatch` is not a global scope but `matchWith` is then `toMatch` definitely does not cover `matchWith`.
	if matchWith.GetGlobalScope() != nil {
		return false
	}

	// If `toMatch` does not have global scope according the v2 representation but `matchWith` does `toMatch` definitely does not cover `matchWith`.
	if V2GlobalScope(matchWith) {
		return false
	}

	if matchWith.GetImageScope() == nil {
		return false
	}

	toMatchImgScope := toMatch.GetImageScope()
	matchWithImgScope := matchWith.GetImageScope()
	return matchWithImgScope.GetRegistry() == toMatchImgScope.GetRegistry() &&
		matchWithImgScope.GetRemote() == toMatchImgScope.GetRemote() &&
		(matchWithImgScope.GetTag() == toMatchImgScope.GetTag() || toMatchImgScope.GetTag() == common.MatchAll)
}
